﻿using System;
using System.Collections;
using System.Collections.Specialized;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using iFinity.DNN.Modules.UrlMaster;
using iFinity.DNN.Modules.BlogFriendlyUrlProvider.Entities;
namespace iFinity.DNN.Modules.BlogFriendlyUrlProvider
{
    /// <summary>
    /// This is a custom Url provider for the DotNetNuke Blog Module.
    /// It creates a different style of Url for the DNN Blog module.
    /// This is a plug-in for the Url Master module.
    /// </summary>
    /// <remarks>
    /// This module is also intended as an example project for developers wishing to write their own module provider
    /// </remarks>
    [Serializable()]
    public class BlogModuleProvider : iFinity.DNN.Modules.UrlMaster.ModuleFriendlyUrlProvider
    {
        #region private members
        FriendlyUrlOptions _options = null; //local copy of the Friendly Url Options, which control things like the inclusion of hyphens instead of spaces, etc.

        //the following set of members are specific to this module, and are used to control how the module provider works.
        //these are read from the web.config file, and appear as attributes in the Blog Module Provider declaration
        BlogTitleOptions _blogTitle = BlogTitleOptions.Title; // controls how the module title is used to generate the blog home Url.
        protected int _noDnnPagePathTabId=-1; //- used to specify if the blog module is on a page where the page path should not be included in the url.
        protected string _tagPath; //- specifies the url path to use for specifying a tags Url
        protected string _catPath; //- specifies the url path to use for specifying a category Url
        protected string _postPath;//- specifies the url path to use for a blog entry Url
        protected string _datePath;//- specifies the url path to use for a date Url
        protected bool _return404ForMissingEntries = false; // - option to return a 404 error if a non-existent blog entry is requested.
        protected int _startingEntryId = 0;//- option to only start friendly urls from an existing entry id (to prevent redirecting all existing entries, applies new urls to new posts only)
        protected string _replaceChars = null;
        private const string DefaultReplaceChars = ";,\"+";//default values for backwards compatibility
        protected bool _doubleSpaceReplacement = false;//default false for backwards compatibility
        //enum for controlling title display
        internal enum BlogTitleOptions
        {
            Username, Display_Name, None, Title
        }
        #endregion

        #region public overriden members
        /// <summary>
        /// Constructor for the Blog Module Url Provider.  This is called by the Url Master module when loading the Provider
        /// </summary>
        /// <param name="name">Name is supplied from the web.config file, and specifies the unique name of the provider</param>
        /// <param name="attributes">Attributes are the xml attributes from the file</param>
        /// <param name="portalId">The portalId is supplied for the calling portal.  Each instance of the provider is portal specific.</param>
        public BlogModuleProvider(string name, NameValueCollection attributes, int portalId)
            : base(name, attributes, portalId)
        {
            //In the constructor, the attributes are read from the attributes collection
            //and converted into the module options

            //set option for showAUthor, read from the web.config file
            string rawBlogTitle = attributes["blogTitle"];
            _blogTitle = ConvertFromString(rawBlogTitle);
            
            //check the attribute for returning a 404 when an entry isn't found
            string raw = attributes["return404ForMissingEntries"];
            bool.TryParse(raw, out _return404ForMissingEntries);
            //look for an attribute specifying which tab the dnn blog
            //will not use a page path for.  There can only be one
            //tab specified per portal (because there is no page path, then it
            //can only be a single page, as the way of determining one dnn
            //page from another isn't in the reuqested Url)
            string noDnnPagePathTabRaw = attributes["noDnnPagePathTabId"];
            int.TryParse(noDnnPagePathTabRaw, out _noDnnPagePathTabId);
            _postPath = attributes["postPath"];
            _catPath = attributes["catPath"];
            _tagPath = attributes["tagPath"];
            _datePath = attributes["datePath"];
            string startingEntryIdRaw = attributes["startingEntryId"];
            if (int.TryParse(startingEntryIdRaw, out _startingEntryId) == false)
                _startingEntryId = 0;

            //922 : support customisation of the blog provider replacement chars
            string rawReplaceChars = attributes["replaceChars"];
            if (rawReplaceChars != null)
                _replaceChars = rawReplaceChars;
            else
                _replaceChars = " ;,\"+";

            string rawDoubleSpaceReplacement = attributes["doubleSpaceReplacement"];
            if (string.IsNullOrEmpty(rawDoubleSpaceReplacement) == false)
                bool.TryParse(rawDoubleSpaceReplacement, out _doubleSpaceReplacement);
        }
        /// <summary>
        /// Returns any custom settings that are specific per-portal for this instance
        /// </summary>
        /// <remarks>
        /// This is used to provide the inputs for the Provider Portal Settings page.  
        /// </remarks>
        /// <returns>A Dictionary of the key/value pairs of the settings used in this provider.  Return empty dictionary if no portal specific options.</returns>
        public override Dictionary<string, string> GetProviderPortalSettings()
        {
            //returns the settings specific to this portal instance
            Dictionary<string, string> settings = new Dictionary<string, string>();
            //just add each setting to the dictionary, using the attribute name as the key and the value converted to a string.
            string blogTitle = null;
            switch (_blogTitle)
            {
                case BlogTitleOptions.None:
                    blogTitle = "none";
                    break;
                case BlogTitleOptions.Display_Name:
                    blogTitle = "displayname";
                    break;
                case BlogTitleOptions.Username:
                    blogTitle = "username";
                    break;
                case BlogTitleOptions.Title:
                    blogTitle = "title";
                    break;
            }
            settings.Add("blogTitle", blogTitle);

            if (_postPath != null && _postPath != "")
                settings.Add("postPath", _postPath);
            if (_catPath != null && _catPath != "")
                settings.Add("catPath", _catPath);
            if (_datePath != null && _datePath != "")
                settings.Add("datePath", _datePath);
            if (_tagPath != null && _tagPath != "")
                settings.Add("tagPath", _tagPath);
            if (_return404ForMissingEntries)
                settings.Add("return404ForMissingEntries", "true");//default false, so only save 'true' value
            else
                settings.Add("return404ForMissingEntries", null);//adding a null value removes the setting from the config
            if (_noDnnPagePathTabId > 0)
                settings.Add("noDnnPagePathTabId", _noDnnPagePathTabId.ToString());
            
            if (_startingEntryId > 0)//default 
                settings.Add("startingEntryId", _startingEntryId.ToString());

            if (_replaceChars != null && _replaceChars != DefaultReplaceChars)
                settings.Add("replaceChars", _replaceChars);
            else
                settings.Add("replaceChars", null);

            if (_doubleSpaceReplacement)
                settings.Add("doubleSpaceReplacement", "true");
            else
                settings.Add("doubleSpaceRepacement", null);

            return settings;
        }
        /// <summary>
        /// Returns the friendly name of the provider.
        /// </summary>
        public override string FriendlyName
        {
            get { return "DotNetNuke Blog Module Friendly Url Provider";  }
        }
        /// <summary>
        /// Returns the Path of the Settings .ascx control (if supplied).  The Settings control is loaded by the Portal Urls page for the provider,
        /// and allows interaction of the settings by the end user, at a per-portal level.
        /// </summary>
        /// <remarks>
        /// Return empty or null string if no control exists.  The path should match the path specified in the DNN install manifest, and 
        /// point to an actual file.
        /// </remarks>
        public override string SettingsControlSrc
        {
            get { return "DesktopModules/iFinity.BlogFriendlyUrlProvider/Settings.ascx";  }
        }
        /// <summary>
        /// The Change Friendly Url method is called for every Url generated when a page is generated by DotNetNuke.  This call sits 'underneath' the 'NavigateUrl' call in DotNetNuke.
        /// Whenever your module calls NavigateUrl, this method will be also called.  In here, the module developer should modify the friendlyUrlPath to the final state required.
        /// However, because this call is used for all Urls on the page, not just those generated by the target module, some type of high-level filter should be used to make sure only
        /// the module Urls are modified.
        /// 
        /// In this example the Blog Module Urls are isolated and modified.
        /// </summary>
        /// <param name="tab">Current Tab</param>
        /// <param name="friendlyUrlPath">Current Friendly Url Path after going through the Friendly Url Generation process of the Url Master module.</param>
        /// <param name="options">The options currently applying to Urls in this portal (space replacement, max length, etc)</param>
        /// <param name="cultureCode">The culture code being used for this Url (if supplied, may be empty)</param>
        /// <param name="endingPageName">The page name the Url has been called with. Normally default.aspx, but may be different for some modules.  In the case of the blog module, this will be a simplified version of the blog entry title.</param>
        /// <param name="useDnnPagePath">Out parameter to be set by the module.  If true, the path of the DNN page will be in the Url (ie /blog).  If false, this part of the Url will be removed. </param>
        /// <param name="messages">List of debug messages.  Add any debug information to this collection to help debug your provider.  This can be seen in the repsonse headers, and also in the 'test Url Rewrite' page in the Url Master module.</param>
        /// <returns></returns>
        public override string ChangeFriendlyUrl(DotNetNuke.Entities.Tabs.TabInfo tab, string friendlyUrlPath, FriendlyUrlOptions options, string cultureCode, ref string endingPageName, out bool useDnnPagePath, ref List<string> messages)
        {
            useDnnPagePath = true;//we want to keep the DNN page name in the Urls for this module.
            
            if (messages != null) messages = new List<string>();
            //first check to see if this Url is an 'edit' Url - something that loads a module-specific page.
            //we don't want to mess with these, because they're always permissions based Urls and thus
            //no need to be friendly
            if (Regex.IsMatch(friendlyUrlPath, @"/?(mid|moduleId)/\d+/?", RegexOptions.IgnoreCase) == false)
            {
                _options = CustomiseOptionsForProvider(options);
                //README : while this procedure is quite long, it is all included in one group for completeness.
                //The basic logic is to match the requested Url with one of the known Blog module patterns (the original patterns as created by the blog module)
                //each pattern is tried, and if a pattern match is found, then the appropriate lookup in the dictionary is done
                //if a match is found in the dictionary, the replacement value is used instaed of the original blog module Url format

                //try and match incoming friendly url path to what we would expect from a blog entry
                Regex entryRegex = new Regex(@"(?<l>/)?EntryId/(?<entryId>\d+)", RegexOptions.IgnoreCase);
                Match entryMatch = entryRegex.Match(friendlyUrlPath);
                Hashtable friendlyUrlIndex = null;
                if (entryMatch.Success)
                {
                    //yes, this is for a blog entry
                    //do something to make this friendly url path better
                    string rawId = entryMatch.Groups["entryId"].Value;
                    int entryId = 0;
                    if (int.TryParse(rawId, out entryId))
                    {
                        if (entryId >= _startingEntryId)//only for entries that are beyond the starting point for the blog entries
                        {
                            //use the id to lookup a more friendly url and use that to modify the friendlyUrlPath variable
                            friendlyUrlIndex = UrlController.GetBlogFriendlyUrlIndex(tab.TabID, tab.PortalID, _startingEntryId, this);
                            if (friendlyUrlIndex != null)
                            {
                                string furlkey = "e" + entryId.ToString(); //entry urls are indexed with e + entryId ("e5")
                                string path = (string)friendlyUrlIndex[furlkey];//check if in the index
                                if (path == null)
                                {
                                    //don't normally expect to have a no-match with a friendly url path when an entryId was in the Url.
                                    //do a direct call and find out if it's there
                                    //950 : change to direct access to work around blog module dependency
                                    BlogEntryInfo entry = UrlController.GetEntry(entryId);
                                    messages.Add("EntryId not found : " + entryId.ToString() + " Checking blog entries directly");
                                    if (entry != null)
                                    {
                                        messages.Add("EntryId found : " + entryId.ToString() + " Rebuilding indexes");
                                        //call and get the path
                                        path = UrlController.MakeBlogEntryFriendlyUrl(entry.Title, entry.AddedDate, this, _options);
                                        //so this entry did exist but wasn't in the index.  Rebuild the index
                                        UrlController.RebuildBlogIndexes(tab.TabID, tab.PortalID, _startingEntryId, this);
                                    }
                                }
                                if (path != null)
                                {
                                    //url found in the index for this entry.  So replace the matched part of the path with the friendly url
                                    if (entryMatch.Groups["l"].Success) //if the path had a leading /, then make sure to add that onto the replacement
                                        path = base.EnsureLeadingChar("/", path);

                                    messages.Add("Blog Entry Friendly Url Replacing : " + friendlyUrlPath + " in Path : " + path);
                                    //replace the path in the path - which leaves any other parts of a path intact.
                                    friendlyUrlPath = entryRegex.Replace(friendlyUrlPath, path);//replace the part in the friendly Url path with it's replacement.
                                    //check if this tab is the one specified to not use a path
                                    if (_noDnnPagePathTabId == tab.TabID)
                                        useDnnPagePath = false;//make this Url relative from the site root

                                    //blog module puts entry title into the 'endingPageName' value.  We're handling addition of the page title in this provider, so 
                                    //set back to default.aspx so that Url Master removes it.
                                    endingPageName = DotNetNuke.Common.Globals.glbDefaultPage;
                                }
                            }
                        }
                    }
                }
                else
                {
                    //look for catetgory match
                    Regex catRegex = new Regex(@"(?<l>/)?catid/(?<catid>\d+)", RegexOptions.IgnoreCase);
                    Match catMatch = catRegex.Match(friendlyUrlPath);
                    if (catMatch.Success)
                    {
                        //looks like a cat path
                        string rawId = catMatch.Groups["catid"].Value;
                        int catId = 0;
                        if (int.TryParse(rawId, out catId))
                        {
                            //ok valid catid, lets look up the index
                            if (friendlyUrlIndex == null)
                                    friendlyUrlIndex = UrlController.GetBlogFriendlyUrlIndex(tab.TabID, tab.PortalID, _startingEntryId, this);
                            if (friendlyUrlIndex != null)
                            {
                                string furlKey = "c" + rawId;
                                string path = (string)friendlyUrlIndex[furlKey];
                                if (path != null) //Found!
                                {
                                    if (catMatch.Groups["l"].Success) //if the path had a leading /, then make sure to add that onto the replacement
                                        path = base.EnsureLeadingChar("/", path);
                                    messages.Add("Blog Category Friendly Url Replacing : " + friendlyUrlPath + " in Path : " + path);
                                    friendlyUrlPath = catRegex.Replace(friendlyUrlPath, path);//replace section of friendly url path with the category friendly url

                                    //blog module puts the category 'slug' into the page value in the url
                                    //set back to default.aspx so that Url Master removes it.
                                    endingPageName = DotNetNuke.Common.Globals.glbDefaultPage; 
                                    
                                    //check if this tab is the one specified to not use a path
                                    if (_noDnnPagePathTabId == tab.TabID)
                                        useDnnPagePath = false;//make this Url relative from the site root


                                }
                            }
                        }
                    }
                    else
                    {
                        //look for a tag Url match
                        Regex tagRegex = new Regex(@"(?<l>/)?tagid/(?<tagid>\d+)", RegexOptions.IgnoreCase);
                        Match tagMatch = tagRegex.Match(friendlyUrlPath);
                        if (tagMatch.Success)
                        {
                            //looks like a tag path
                            string rawId = tagMatch.Groups["tagid"].Value;
                            int tagId = 0;
                            if (int.TryParse(rawId, out tagId))
                            {
                                //ok valid tagid, lets look up the index
                                if (friendlyUrlIndex == null)
                                    friendlyUrlIndex = UrlController.GetBlogFriendlyUrlIndex(tab.TabID, tab.PortalID, _startingEntryId, this);
                                if (friendlyUrlIndex != null)
                                {
                                    string furlKey = "t" + rawId;//tag index is t+tagid
                                    string path = (string)friendlyUrlIndex[furlKey];
                                    if (path != null) //Found!
                                    {
                                        if (tagMatch.Groups["l"].Success) //if the path had a leading /, then make sure to add that onto the replacement
                                            path = base.EnsureLeadingChar("/", path);
                                        messages.Add("Blog Tag Friendly Url Replacing : " + friendlyUrlPath + " in Path : " + path);
                                        friendlyUrlPath = tagRegex.Replace(friendlyUrlPath, path);//replace section of friendly url path with the tag friendly url

                                        //blog module puts the tag 'slug' into the page value in the url
                                        //set back to default.aspx so that Url Master removes it.
                                        endingPageName = DotNetNuke.Common.Globals.glbDefaultPage;

                                        //check if this tab is the one specified to not use a path
                                        if (_noDnnPagePathTabId == tab.TabID)
                                            useDnnPagePath = false;//make this Url relative from the site root

                                     }
                                }
                            }
                        }
                        else
                        {

                            //not an entry, category or tag? - check for a blog path pattern match
                            //this regex looks for the /blogId/ pattern, but not when the /moduleid/xx or /mid/xx pattern is also there
                            Regex blogRegex = new Regex(@"(?<l>/)?BlogId/(?<blogId>\d+)", RegexOptions.IgnoreCase);
                            Match blogMatch = blogRegex.Match(friendlyUrlPath);
                            if (blogMatch.Success)
                            {
                                //yes, looks like a blog path
                                string rawId = blogMatch.Groups["blogId"].Value;
                                int blogId = 0;
                                if (int.TryParse(rawId, out blogId))
                                {
                                    //use the blog id to look up
                                    if (friendlyUrlIndex == null)
                                        friendlyUrlIndex = UrlController.GetBlogFriendlyUrlIndex(tab.TabID, tab.PortalID, _startingEntryId, this);
                                    if (friendlyUrlIndex != null)
                                    {
                                        string furlKey = "b" + rawId;//the key for the blog Urls is 'b' + blogId - to uniquely separate from entry urls
                                        string path = (string)friendlyUrlIndex[furlKey];
                                        if (path != null)
                                        {
                                            if (blogMatch.Groups["l"].Success) //if the path had a leading /, then make sure to add that onto the replacement
                                                path = base.EnsureLeadingChar("/", path);
                                            messages.Add("Blog Friendly Url Replacing : " + friendlyUrlPath + " in Path : " + path);
                                            friendlyUrlPath = blogRegex.Replace(friendlyUrlPath, path);//replace section of friendly url path with the blog friendly url
                                        }
                                    }
                                }
                            }
                            else
                            {
                                //not an entry, category, tag or blog Url.
                                //now check to see if it is a date based Url
                                Regex dateRegex = new Regex(@"(?:/)?BlogDate/(?<dt>\d{4}-\d{1,2}-\d{1,2})/DateType/(?<tp>month|day|year)", RegexOptions.IgnoreCase);
                                Match dateMatch = dateRegex.Match(friendlyUrlPath);
                                if (dateMatch.Success)
                                {
                                    //looks like a date path
                                    string rawDate = dateMatch.Groups["dt"].Value;
                                    string rawType = dateMatch.Groups["tp"].Value;
                                    DateTime urlDate;
                                    if (DateTime.TryParse(rawDate, out urlDate))
                                    {
                                        //it's a winner
                                        //take /BlogDate/yyyy-mm-dd/DateType/month into /yyyy/mm/dd
                                        string path = "/" + urlDate.Year.ToString();
                                        switch (rawType.ToLower())
                                        {
                                            case "year":
                                                //do nothing, already have it
                                                break;
                                            case "month":
                                                path += "/" + urlDate.Month.ToString("00");
                                                break;
                                            case "day":
                                                path += "/" + urlDate.Month.ToString("00");
                                                path += "/" + urlDate.Day.ToString("00");
                                                break;
                                        }
                                        //put on date path if it is specified
                                        if (_datePath != null && _datePath != "")
                                            path = "/" + _datePath + path;
                                        //check if this tab is the one specified to not use a path
                                        if (_noDnnPagePathTabId == tab.TabID)
                                            useDnnPagePath = false;//make this Url relative from the site root
                                        messages.Add("Blog Friendly Url Replacing : " + friendlyUrlPath + " with date format Url: " + path);
                                        friendlyUrlPath = dateRegex.Replace(friendlyUrlPath, path); //replace section with new date path
                                    }
                                }
                            }
                        }
                    }
                }
            }
            return friendlyUrlPath;
        }

        /// <summary>
        /// This method is used by the Url Master Url Rewriting process.  The purpose of this method is to take the supplied array of Url parameters, and transform them into a module-specific querystring for the underlying re-written Url.
        /// In the case of the blog module, this takes the /yyyy/mm/dd/blog-title values (as an array, ie [yyyy,mm,dd,blog-title]) and converts it into &entryId=zz, which is then used as an input into the rewriting process.
        /// </summary>
        /// <param name="urlParms">The array of parameters found after the DNN page path has been identified.  No key/valeu pairs are identified, the parameters are converted from the /key/value/key2/value2 format into [key,value,key2,value2] format.</param>
        /// <param name="tabId">TabId of identified DNN page. </param>
        /// <param name="portalId">PortalId of identified DNN portal.</param>
        /// <param name="options">The current Friendly Url options being used by the module.</param>
        /// <param name="cultureCode">Identified language/culture code, if supplied.</param>
        /// <param name="portalAlias">Identified portalAlias object for the request.</param>
        /// <param name="messages">List of debug messages.  Add to this list to help debug your module.  Can be viewed in the reponse headers of the request, or in the 'Test Url Rewriting' section of the Url Master module.</param>
        /// <param name="status">Out parameter, returns the Http status of the request.  May be 200,301,302, or 404.  For normal rewriting, return a 200 value.</param>
        /// <param name="location">If a 301 or 302 is returned in the status parameter, then this must contain a valid redirect location.  This should be a fully-qualified Url.</param>
        /// <returns>The querystring to be used for rewriting the Url. NOTE: doesn't need to include the tabid if the tabid parameter is > -1</returns>
        public override string TransformFriendlyUrlToQueryString(string[] urlParms, int tabId, int portalId, DNN.Modules.UrlMaster.FriendlyUrlOptions options, string cultureCode, DotNetNuke.Entities.Portals.PortalAliasInfo portalAlias, ref List<string> messages, out int status, out string location)
        {

            string result = ""; status = 200; //OK 
            location = null; //no redirect location
            if (messages == null) messages = new List<string>();
            
            //README : this method works for the blog module by comparing the path supplied against the known patterns as defined by this module provider
            //if a match is found, then the friendly Url is matched against the dictionary which contains friendlyurls as the key, and the querystring as the value.
            //WHen a match is found, it's just a case of incorporating the querystring into the returned result

            //look for match on pattern for date and title - the pattern used by this provider
            string path = string.Join("/", urlParms);
            Hashtable queryStringIndex = null;
            int skipUpToIndex = -1;
            bool found = false;
            bool siteRootMatch = false;
            //tabid == -1 when no dnn page path is in the Url.  This means the Url Master module can't determine the DNN page based on the Url.
            //In this case, it is up to this provider to identify the correct tabId that matches the Url.  Failure to do so will result in the incorrect tab being loaded when the page is rendered. 
            bool checkForProcessing = true;
            if (tabId == -1)
            {
                siteRootMatch = true;
                if (_noDnnPagePathTabId > -1)
                    //tabid -1 means a 'site root' match - meaning that the dnn page path wasn't included in the Url
                    tabId = _noDnnPagePathTabId;//if tabid = -1, it means a site root match (no dnn page path) so we substitute in the tabid where this is being used
            }
            else
            {
                //now, because this provider allows for noDnnPagePath operation, it will be loaded on every request, for every tab.
                //this means it could process the wrong urls (ie, Urls that don't belong to the blog module)
                //so we implement our own tab based filtering.  When a tab has been identified by the Url Master module (ie, the tab path was in the url)
                //then we will ignore the request unless the tab is specifically included in the list, or there are no specific tabs selected in the configuration
                if (this.TabIds.Count > 0 &&  this.TabIds.Contains(tabId) == false)//provider is not configured to process on this tab, so don't do anything
                {
                    //we don't want to do anything
                    checkForProcessing = false;
                }
            }
            
            if (checkForProcessing)
            {
                _options = CustomiseOptionsForProvider(options);
                //OK, we've decided that there's a good chance this Url could be a blog module url
                messages.Add("Checking for Blog Entries in Blog Friendly Url Provider - path: " + path);
                //start by looking for the pattern of entries - /yyyy/mm/dd/blog-post-name
                string entryRegexPattern = @"(/)?(?<dt>\d{4}/\d{1,2}/\d{1,2})/[^/]+";
                if (_postPath != null)
                    entryRegexPattern = string.Format("(?<path>{0})?{1}", _postPath, entryRegexPattern);//add on post path if used
                Match entryMatch = Regex.Match(path, entryRegexPattern, RegexOptions.IgnoreCase);//match pattern with requested path
                if (entryMatch.Success)
                {

                    //matches the date pattern
                    string entryUrl = entryMatch.Value.ToLower();
                    //none of the indexes use a leading /
                    entryUrl = base.EnsureNotLeadingChar("/", entryUrl);

                    //try and find the entry in the index
                    queryStringIndex = UrlController.GetBlogQueryStringIndex(tabId, portalId, _startingEntryId, this, false);
                    if (queryStringIndex != null)
                    {
                        string pathMatchValue = entryMatch.Groups["path"].Value;
                        //1144: if there was no match in the path, but there was a post path, put it back on
                        //this is because it's possible that the post path matches a tab path, and can get
                        //incorrectly recognised as a page
                        if (string.IsNullOrEmpty(pathMatchValue) && _postPath != null)
                            entryUrl = base.EnsureNotLeadingChar("/", _postPath + "/" + entryUrl);

                        //check the querystring index for the entry    
                        if (queryStringIndex.ContainsKey(entryUrl))
                        {
                            found = true;
                            skipUpToIndex = 3;
                            //don't process the first 4 parameters, because we are replacing them manually from the index match
                            string entryQs = (string) queryStringIndex[entryUrl];
                            messages.Add("Blog Entry Matched in Blog Url Provider.  Url : " + entryUrl + " Path : " +
                                            path + " Entry qs : " + entryQs);
                            result += entryQs;
                        }
                        
                    }
                    if (!found)
                    {
                        //check for entry based on date
                        string rawDate = entryMatch.Groups["dt"].Value;
                        DateTime postDate;
                        if (DateTime.TryParse(rawDate, out postDate))
                        {
                            TimeSpan postAge = DateTime.Now - postDate;
                            if (postAge.Hours < 24)
                            {
                                //posted in the last 24 hours, maybe just need a refresh in the index
                                messages.Add("Post not found (" + entryUrl + ") but post date calculated to be " + postDate.ToShortDateString() + " - rebuilding index.");
                                queryStringIndex = UrlController.GetBlogQueryStringIndex(tabId, portalId, _startingEntryId, this, true);
                                if (queryStringIndex.ContainsKey(entryUrl))
                                {
                                    found = true;
                                    messages.Add("Blog Entry Matched in Blog Friendly Url Provider after rebuild.  EntryUrl : " + entryUrl + " Path : " + path);
                                    skipUpToIndex = 3;//don't process the first 4 parameters, because we are replacing them manually from the index match
                                    string entryQs = (string)queryStringIndex[entryUrl];
                                    result += entryQs;
                                }
                            }
                        }
                    }

                    if (!found && _return404ForMissingEntries)
                    {
                        //returning a 404 because the blog entry wasn't found, and it did match the correct pattern
                        messages.Add("Blog Entry not matched in Blog Friendly Url Provider.  Path : " + path + " Returning 404");
                        status = 404;
                    }
                }

                if (!found)
                {
                    //no match for the blog entry?  Perhaps maybe it's the blog itself, based on the authors display name or blog title
                    //no regex for this one - just try looking up the first value of the params 
                    if (urlParms.GetUpperBound(0) >= 0)
                    {
                        if (queryStringIndex == null)
                            queryStringIndex = UrlController.GetBlogQueryStringIndex(tabId, portalId, _startingEntryId, this, false);
                        if (queryStringIndex != null)
                        {
                            string firstParm = urlParms[0].ToLower();//no case sensitivity on urls
                            if (urlParms.GetUpperBound(0) > 0 && _catPath != null && _catPath.ToLower() == firstParm)
                            {
                                //could be a category
                                string category = firstParm + "/" + urlParms[1].ToLower();//category
                                if (queryStringIndex.ContainsKey(category))
                                {
                                    messages.Add("Category matched in Blog Friendly Url Provider. Cat Url :" + category + " Path:" + path);
                                    skipUpToIndex = 1;
                                    result += (string)queryStringIndex[category];
                                    found = true;
                                }
                            }

                            if (!found && urlParms.GetUpperBound(0) > 0 && _tagPath != null && _tagPath.ToLower() == firstParm)
                            {
                                //could be a tag path
                                string tag = firstParm + "/" + urlParms[1].ToLower();//tag path
                                if (queryStringIndex.ContainsKey(tag))
                                {
                                    messages.Add("Tag matched in Blog Friendly Url Provider. Tag Url :" + tag + " Path:" + path);
                                    skipUpToIndex = 1;
                                    result += (string)queryStringIndex[tag];
                                    found = true;
                                }
                            }
                            if (!found)
                            {
                                //now check for any matching blog entry (could be blog, tag or category)
                                string blogUrlName = firstParm;
                                if (queryStringIndex.ContainsKey(blogUrlName))
                                {
                                    messages.Add("Blog matched in Blog Friendly Url Provider.  BlogUrl : " + blogUrlName + " Path : " + path);
                                    skipUpToIndex = 0;//skip the first position in the parameters
                                    string blogQs = (string)queryStringIndex[blogUrlName];
                                    result += blogQs;
                                }
                            }

                            if (!found)
                            {
                                //no match or no index.  Let's look at entry id or blog id
                                if (_return404ForMissingEntries && urlParms.GetUpperBound(0) >= 1)
                                {
                                    int entryId = -1;
                                    if (urlParms[0].ToLower() == "entryid" && int.TryParse(urlParms[1], out entryId))
                                    {
                                        //first two parms are /entryid/xx and xx is a valid int.
                                        //lets check to see if it's a real entry
                                        //950 : change to direct access to work around blog module dependency
                                        BlogEntryInfo entry = UrlController.GetEntry(entryId);
                                        if (entry == null)
                                        {
                                            //entry id not found, returning 404
                                            messages.Add("Blog Entry not found.  Path : " + path + ", EntryId :" + entryId.ToString() + " Returning 404");
                                            status = 404;
                                        }
                                    }
                                }
                                if (urlParms.GetUpperBound(0) >= 1)
                                {
                                    string key = urlParms[0];
                                    string value = urlParms[1];
                                    int id = -1;
                                    if (int.TryParse(value, out id))
                                    {
                                        //check for either blogId or category id
                                        if (key.ToLower() == "blogid")
                                        {
                                            //this is an old Url pointing to the blog id
                                            //950 : change to direct access to work around blog module dependency
                                            Entities.BlogInfo miniBlog = UrlController.GetBlog(portalId, id);
                                            if (miniBlog != null)
                                            {
                                                bool isFriendly = false;
                                                //906 : 301 loop if 'no title' option chosen, because no friendly version is ever added
                                                string friendlyUrl = UrlController.MakeBlogFriendlyUrl(miniBlog, this, _options, out isFriendly);
                                                if (isFriendly)
                                                {
                                                    location = UrlController.FriendlyUrl(tabId, portalId, portalAlias, "", friendlyUrl);
                                                    status = 301;
                                                    messages.Add("Redirecting old Blog Url : " + path + " to new Blog Url " + location + " because old style blog url");
                                                }
                                            }
                                            else
                                            {
                                                messages.Add("Blog requested : " + path + " with id " + id.ToString() + " was not found");
                                            }
                                        }
                                    }
                                    else
                                    {
                                        if (key.ToLower() == "blogdate")
                                        {
                                            //date based redirect
                                            DateTime blogDateValue = DateTime.MinValue;
                                            if (DateTime.TryParse(value, out blogDateValue))
                                            {
                                                //this is a valid date/time value for redirection
                                                if (urlParms.GetUpperBound(0) >= 3)
                                                {
                                                    string typeKey = urlParms[2];
                                                    string typeValue = urlParms[3];
                                                    if (typeKey.ToLower() == "datetype")
                                                    {
                                                        if (typeValue.Contains(","))
                                                        {
                                                            //duplicate value, sometimes happens
                                                            typeValue = typeValue.Split(',')[0];
                                                        }
                                                        string friendlyUrl = _datePath;
                                                        switch (typeValue.ToLower())
                                                        {
                                                            case "year":
                                                                //friendlyUrl += "/" + blogDateValue.Year.ToString();
                                                                //location = UrlController.FriendlyUrl(tabId, portalId, portalAlias, "", _datePath, blogDateValue.Year.ToString());
                                                                location = UrlController.FriendlyUrl(tabId, portalId,
                                                                                                     portalAlias, "",
                                                                                                     "BlogDate",
                                                                                                     blogDateValue.ToString("yyyy-MM-dd"),
                                                                                                     "dateType", "year");
                                                                break;
                                                            case "month":
                                                                //friendlyUrl += "/" + blogDateValue.Year.ToString() + "/" + blogDateValue.Month.ToString("00");
                                                                //location = UrlController.FriendlyUrl(tabId, portalId, portalAlias, "", _datePath, blogDateValue.Year.ToString(), blogDateValue.Month.ToString("00"));
                                                                //calls back into the friendly url provider to get the correct url
                                                                location = UrlController.FriendlyUrl(tabId, portalId,
                                                                                                     portalAlias, "",
                                                                                                     "BlogDate",
                                                                                                     blogDateValue.ToString("yyyy-MM-dd"),
                                                                                                     "dateType", "month");
                                                                break;
                                                            case "day":
                                                                //friendlyUrl += "/" + blogDateValue.Year.ToString() + "/" + blogDateValue.Month.ToString("00") + "/" + blogDateValue.Day.ToString("00");
                                                                //location = UrlController.FriendlyUrl(tabId, portalId, portalAlias, "", _datePath, blogDateValue.Year.ToString(), blogDateValue.Month.ToString("00"), blogDateValue.Day.ToString("00"));
                                                                location = UrlController.FriendlyUrl(tabId, portalId,
                                                                                                     portalAlias, "",
                                                                                                     "BlogDate",
                                                                                                     blogDateValue.ToString("yyyy-MM-dd"),
                                                                                                     "dateType", "day");
                                                                break;
                                                        }
                                                        status = 301;
                                                        messages.Add("Redirecting " + path + " to " + location + " because using old style date format");
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
                if (!found)
                {
                    //now test for date-based requests
                    if (_datePath == null) _datePath = "";
                    Regex dateRegex = new Regex(@"(^" + _datePath + @"/|^)(?<y>\d{4})(?<m>/\d{1,2})?(?<d>/\d{1,2})?(?<rem>/[^/]+)?");
                    //checks for date based matches
                    Match dateMatch = dateRegex.Match(path);
                    if (dateMatch.Success)
                    {
                        string querystring = "";
                        //date or partial date match
                        string yyyy = dateMatch.Groups["y"].Value;
                        string mm = dateMatch.Groups["m"].Value;
                        string dd = dateMatch.Groups["d"].Value;
                        string rem = dateMatch.Groups["rem"].Value;
                        mm = this.EnsureNotLeadingChar("/", mm);
                        dd = this.EnsureNotLeadingChar("/", dd);
                        rem = this.EnsureNotLeadingChar("/", rem);
                        string blogDate = ""; string dateType = "";
                        if (yyyy != null)
                        {
                            //always starts with the year
                            if (mm != null && mm != "")
                                blogDate = yyyy + "-" + mm;//month found
                            else
                                blogDate = yyyy + "-01";//otherwise jan

                            if (dd != null && dd != "")
                                blogDate += "-" + dd;//day found
                            else
                            {
                                int year, month, day;
                                if (int.TryParse(yyyy, out year) && int.TryParse(mm, out month))
                                {
                                    DateTime monthStart = new DateTime(year, month, 1);
                                    DateTime monthEnd = monthStart.AddMonths(1).AddDays(-1);//add 1 month to the start of the month and take 1 day and you get the end of the month
                                    blogDate += "-" + monthEnd.Day.ToString();//day portion of month end
                                }
                                else
                                    blogDate += "-01";//otherwise first of month
                            }

                            if ((mm == null || mm == "") && (dd == null || dd == ""))
                            {
                                dateType = "month";//Hack alert : dnn blog module doesn't support a 'year' filter on datetype
                                skipUpToIndex = 1;//just year
                            }
                            else
                                if (mm != null && mm.Length > 0 && (dd == null || dd == ""))
                                {
                                    dateType = "month";
                                    skipUpToIndex = 2;//year/month
                                }
                                else
                                    if (mm != null && mm.Length > 0 && dd != null && dd.Length > 0)
                                    {
                                        dateType = "day";
                                        skipUpToIndex = 3;//year/month/day
                                    }

                            if (dateMatch.Groups["rem"].Success)
                            {
                                if (rem.ToLower() == "default.aspx" || rem.ToLower() == "default")
                                    skipUpToIndex++;//take one more off the url
                            }

                            messages.Add("Matched Blog Date field : " + path + " BlogDate " + blogDate + " DateType: " + dateType);
                        }
                        querystring += "&BlogDate=" + blogDate + "&DateType=" + dateType;
                        if (siteRootMatch && _noDnnPagePathTabId > 0)
                            result += "?TabId=" + _noDnnPagePathTabId.ToString();
                        result += querystring;
                    }
                }
            }
            if (result != "") //only change if something was done with the url - because returning an empty result means no specific rewrite was done
            {
                //put on any remainder of the path that wasn't to do with the blog name
                string remainder = base.CreateQueryStringFromParameters(urlParms, skipUpToIndex);
                //put it all together for the final rewrite string
                result += remainder;
            }
            return result;
        }

        /// <summary>
        /// Determines when to do a redirect.  This is separate to the rewriting process.  The module developer can create any type of Url redirect here, because the entire Url of the original request is passed in.
        /// </summary>
        /// <param name="tabId">Identified TabId, if known.  -1 if no valid tabid identified.</param>
        /// <param name="portalid">Identified portalId.</param>
        /// <param name="httpAlias">Identified httpAlias of the request.</param>
        /// <param name="requestUri">The original requested Url</param>
        /// <param name="queryStringCol">The querystring collection of the original request</param>
        /// <param name="options">The friendly url options that currently apply.</param>
        /// <param name="redirectLocation">Out parameter that shows where to redirect to.</param>
        /// <param name="messages">List of messages for debug purposes.  Add to this list to help debug your module.</param>
        /// <returns>true if 301 redirect is required, false if not.  If true, the redirectLocation value must be a valid fully qualified Url.</returns>
        public override bool CheckForRedirect(int tabId, int portalid, string httpAlias, Uri requestUri, NameValueCollection queryStringCol, FriendlyUrlOptions options, out string redirectLocation, ref List<string> messages)
        {
            bool doRedirect = false;
            redirectLocation = "";//set blank location
            //look for specific querystring pattern which relates to former blogengine tags
            string raw = queryStringCol["tag"];
            if (raw != null)
            {
                //check original format
                if (requestUri != null && requestUri.Query.ToLower().Contains("tag"))
                {
                    //remove the leading /
                    string tagValue = base.EnsureNotLeadingChar("/", raw);
                    //tag was in the querystring 
                    Entities.TagInfo tag = new Entities.TagInfo();
                    tag.Tag = tagValue;
                    tag.TagId = 0;//don't know the tagid
                    tag.Slug = tagValue;
                    //get what we think the Url should be for that querystring
                    _options = this.CustomiseOptionsForProvider(options);
                    string tagFriendlyPath = UrlController.MakeTagFriendlyUrl(tag, this, _options);
                    //now substitute that into the friendly url path        
                    string qsQuery = requestUri.Query;
                    //remove the query from overall url, to leave the full path
                    string qsPath = requestUri.AbsoluteUri.Replace(qsQuery, "");
                    //remove the tag from the query part
                    qsQuery = qsQuery.Replace("tag=" + raw, "");
                    if (qsQuery.Length == 1)//if no query left, cancel altogether
                        qsQuery = "";
                    //match up the pattern to find the last part of the Url path (ie, the bit after the last /)
                    Regex lastPathRegex  = new Regex(@"/(?<path>[^/\.]*(?<ext>\.[\w]+)?)?$", RegexOptions.IgnoreCase);
                    Match lastPathMatch = lastPathRegex.Match(qsPath);
                    if (lastPathMatch.Success)
                    {
                        string path = lastPathMatch.Groups["path"].Value;
                        string ext = "";
                        if (lastPathMatch.Groups["ext"].Success)
                        {
                            //there is an extension
                            ext = lastPathMatch.Groups["ext"].Value;
                            path = path.Replace(ext, "");
                        }
                        else
                        {
                            ext = _options.PageExtension;
                        }
                        
                        //ok so have path and extension
                        string newPath = "/" + this.EnsureNotLeadingChar("/", path  + this.EnsureLeadingChar("/", tagFriendlyPath) + ext);
                        string location = lastPathRegex.Replace(qsPath, newPath);//replaces new tag path into location
                        location += this.EnsureLeadingChar("?", qsQuery);
                        //now fold the tag into the path -> awkward!
                        messages.Add("Tag pattern found in Querystring: " + requestUri.Query + " redirecting to " + location);
                        redirectLocation = location;
                        doRedirect = true;
                    }
                }
            }
            return doRedirect;
        }
        /// <summary>
        /// IsLicensed is used to return a licensed/unlicensed status of this provider to the Url Master module.
        /// If the module is not acceptably licensed, the Url Master module will put out a top-of-page message, 
        /// using the messageHtml parameter as input.
        /// </summary>
        /// <param name="requestUri">The original requesting Uri</param>
        /// <param name="messageHtml">out parameter - the licensing message to show</param>
        /// <param name="debugMessage">out parameter - any licensing debug information to add to the debug output of the module.</param>
        /// <returns>true if licensed OK, false if not.</returns>
        public override bool IsLicensed(Uri requestUri, out string messageHtml, out string debugMessage)
        {
            //If this module had a licensing requirement, it would be checked here.  If not licensed
            //return false, and return some Html to be included on any pages where the provider is used, ie:
            //messageHtml = "<div>Hey! this isn't licensed!</div>"; debugMessage = "Fake licence message";
            //return false;

            //but in this case, there's no licensing restrictions with this provider, so always return true
            //and return nothing for the messagehtml and debugmessage
            messageHtml = null; debugMessage = null;
            return true; 
        }
        /// <summary>
        /// This property specifies whether the module Urls will always include the DNN page path.
        /// With DNN Page path : http://example.com/Blog/2001/01/01/blog-title-here.aspx
        /// Without DNN Page path : http://example.com/2001/01/01/blog-title-here.aspx
        /// The above example assumes the module lives on the 'Blog' page.
        /// </summary>
        /// <param name="portalId">Current portalId.  This property is portal-specific.</param>
        /// <returns></returns>
        public override bool AlwaysUsesDnnPagePath(int portalId)
        {
            //The Blog Module Url provider 
            if (_noDnnPagePathTabId > -1)
            {
                return false;//if there is an exception, then return false
            }
            else
                return true;//no specific exceptions, return true every time
        
        }

        #endregion 

        #region internal methods for this provider
        internal BlogTitleOptions ConvertFromString(string value)
        {
            BlogTitleOptions blogTitle = BlogTitleOptions.None ;
            if (value != null)
            {
                switch (value.ToLower())
                {
                    case "none":
                        blogTitle = BlogTitleOptions.None;
                        break;
                    case "displayname":
                    case "display":
                        blogTitle = BlogTitleOptions.Display_Name;
                        break;
                    case "username":
                    case "user":
                        blogTitle = BlogTitleOptions.Username;
                        break;
                    case "title":
                    default:
                        blogTitle = BlogTitleOptions.Title;
                        break;
                }
            }
            return blogTitle;
        }
        internal FriendlyUrlOptions CustomiseOptionsForProvider(FriendlyUrlOptions options)
        {
            FriendlyUrlOptions providerOptions = options.Clone();
            //use reflection to check for properties to allow for backwards compatibility
            System.Reflection.MemberInfo[] replaceDoubleCharsInfoCol = providerOptions.GetType().GetMember("ReplaceDoubleChars");
            if (replaceDoubleCharsInfoCol != null && replaceDoubleCharsInfoCol.Length > 0)
            {
                System.Reflection.MemberInfo replaceDoubleCharsInfo = replaceDoubleCharsInfoCol[0];
                if (replaceDoubleCharsInfo != null && replaceDoubleCharsInfo is System.Reflection.FieldInfo)
                    ((System.Reflection.FieldInfo)replaceDoubleCharsInfo).SetValue(providerOptions, _doubleSpaceReplacement);
            }
            if (_replaceChars != null)
            {
                //options.ReplaceChars = _replaceChars;
                System.Reflection.MemberInfo[] replaceCharsInfoCol = providerOptions.GetType().GetMember("ReplaceChars");
                if (replaceCharsInfoCol != null && replaceCharsInfoCol.Length > 0)
                {
                    System.Reflection.MemberInfo replaceCharsInfo = replaceCharsInfoCol[0];
                    if (replaceCharsInfo != null && replaceCharsInfo is System.Reflection.FieldInfo)
                        ((System.Reflection.FieldInfo)replaceCharsInfo).SetValue(providerOptions, _replaceChars);
                }
            }
            return providerOptions; 
        }

        #endregion
        #region internally exposed properties of provider
        internal BlogTitleOptions BlogTitle
        {
            get
            {   return _blogTitle;}
            set
            {   _blogTitle = value;}
        }
        internal string PostPath
        {
            get
            { return _postPath; }
            set
            { _postPath = value; }

         }
        internal string TagPath
        {
            get
            {  return _tagPath; }
            set
            { _tagPath = value; }
        }
        internal string CatPath
        {
            get
            {   return _catPath;}
            set
            { _catPath = value; }
        }
        internal string DatePath
        {
            get
            {   return _datePath; }
            set
            { _datePath = value; }
        }
        internal int NoDnnPagePathTabId
        {
            get { return _noDnnPagePathTabId; }
            set { _noDnnPagePathTabId = value; }
        }
        internal bool Return404ForMissingEntries
        {
            get { return _return404ForMissingEntries; }
            set { _return404ForMissingEntries = value; }
        }
        internal int StartingEntryId
        {
            get { return _startingEntryId; }
            set { _startingEntryId = value; }
        }
        internal string ReplaceChars
        {
            get { return _replaceChars; }
            set { _replaceChars = value; }
        }
        internal bool ReplaceDoubleSpaces
        {
            get { return _doubleSpaceReplacement; }
            set { _doubleSpaceReplacement = value; }
        }
        internal FriendlyUrlOptions Options
        {
            get
            {
                return _options;
            }
            set
            { _options = value; }
    }
        internal new string CleanNameForUrl(string title, FriendlyUrlOptions options)
        {
            bool replaced = false;
            //the base module Url PRovider contains a Url cleaning routine, which will remove illegal and unwanted characters from a string, using the specific friendly url options
            return base.CleanNameForUrl(title, options, out replaced);
        }
        internal string CleanNameForUrl(string title)
        {
            FriendlyUrlOptions options;
            if (_options == null)
                options = CustomiseOptionsForProvider(new FriendlyUrlOptions());
            else
                options = _options;
            return this.CleanNameForUrl(title, options);
        }
        #endregion
        
    }
}
